Skip to content

Conversation

@bouwew
Copy link
Contributor

@bouwew bouwew commented Sep 21, 2025

In Core testing some mypy errors show up for the Plugwise integration, fixing them first in Plugwise-beta.

Summary by CodeRabbit

  • Bug Fixes

    • Prevented crashes when Plugwise data is missing; sensors, switches, numbers, selects, and climate entities now report “Unavailable” instead of raising errors.
    • Temperatures, setpoints, binary states and options may show as “Unavailable” when underlying data is absent.
    • More graceful handling of unsupported HVAC modes with sensible defaults.
  • Refactor

    • Safer data access and defaulting across the Plugwise integration to improve stability and resilience.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 21, 2025

Walkthrough

Replace direct dict indexing with safe .get(...) access across Plugwise entity modules, making several properties return optional types (allowing None). Harden climate HVAC mode resolution and related temperature/preset lookups to avoid KeyError and invalid-mode exceptions.

Changes

Cohort / File(s) Summary
Safe access — binary / sensor / switch
custom_components/plugwise/binary_sensor.py, custom_components/plugwise/sensor.py, custom_components/plugwise/switch.py
Replace direct dict indexing with self.device.get(..., {}).... Return types made optional: `binary_sensor.is_on -> bool
Climate: safe lookups & HVAC hardening
custom_components/plugwise/climate.py
Replace direct lookups with guarded get(...) (schedules, presets, sensors). Temperature getters now `float
Entity availability
custom_components/plugwise/entity.py
Use self.device.get(AVAILABLE, ...) for availability checks to avoid KeyError.
Number entity: bounds, step, optional value
custom_components/plugwise/number.py
Read actuator/bounds via get(...) with defaults; clamp step safely. native_value now returns `float
Select entity: safe current/options
custom_components/plugwise/select.py
current_option uses get(...) (may be None); options uses get(..., []) to default to an empty list.
Changelog
CHANGELOG.md
Added "Ongoing" section entry: "Fix mypy errors in Core".

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant HA as Home Assistant
  participant Entity as Plugwise Entity
  participant Device as Device Data (dict)

  HA->>Entity: Read property (is_on / native_value / temperature / options)
  Entity->>Device: device.get(primary_key, default) → nested .get(...)
  alt Key present & valid
    Device-->>Entity: value
    Entity-->>HA: value (may be Optional typed)
  else Key missing or invalid
    Device-->>Entity: default (None / [] / fallback)
    Entity-->>HA: None / [] / fallback
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

quality, downstreaming

Suggested reviewers

  • CoMPaTech
  • github-actions

Poem

A hop, a nibble, I check the keys,
Missing ones return with gentle ease.
No crashes here, just optional cheer,
Safe gets and defaults keep things clear.
I thump my paw — the update's here! 🥕🐇

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Fix mypy errors" is concise and accurately reflects the primary intent of the changeset—type adjustments and safer dict access to resolve mypy complaints across the Plugwise integration—so it clearly summarizes the main change for a reviewer scanning history.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-mypy-errors

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
custom_components/plugwise/climate.py (1)

199-210: Bug: comparing string mode to list[HVACMode] always falls back to HEAT.

mode not in self.hvac_modes compares a str to HVACMode members, so it’s always True. Fix by normalizing to HVACMode first.

-        if (
-            mode := self.device.get(CLIMATE_MODE)
-        ) is None or mode not in self.hvac_modes:  # pw-beta add to Core
-            return HVACMode.HEAT  # pragma: no cover
-        # pw-beta homekit emulation
-        if self._homekit_enabled and self._homekit_mode == HVACMode.OFF:
-            mode = HVACMode.OFF  # pragma: no cover
-
-        return HVACMode(mode)
+        mode = self.device.get(CLIMATE_MODE)
+        if mode is None:
+            return HVACMode.HEAT  # pragma: no cover
+        try:
+            hvac = HVACMode(mode)
+        except ValueError:
+            return HVACMode.HEAT  # pragma: no cover
+        if hvac not in self.hvac_modes:
+            return HVACMode.HEAT  # pragma: no cover
+        # pw-beta homekit emulation
+        if self._homekit_enabled and self._homekit_mode == HVACMode.OFF:
+            return HVACMode.OFF  # pragma: no cover
+        return hvac
🧹 Nitpick comments (8)
custom_components/plugwise/entity.py (1)

80-88: Simplify AVAILABLE check for readability (same behavior).

Equivalent, slightly clearer, and keeps mypy happy:

-            and (AVAILABLE not in self.device or self.device.get(AVAILABLE) is True)
+            and (self.device.get(AVAILABLE, True) is True)
custom_components/plugwise/binary_sensor.py (1)

167-178: Tighten return typing to Optional[bool] to avoid Any leaking.

If mypy is in strict mode, consider an explicit cast:

-        return self.device.get(BINARY_SENSORS, {}).get(self.entity_description.key)
+        value = self.device.get(BINARY_SENSORS, {}).get(self.entity_description.key)
+        return cast(bool | None, value)

Add import near existing typing imports:

-from typing import Any
+from typing import Any, cast
custom_components/plugwise/sensor.py (1)

518-522: Constrain native_value to expected numeric types.

Optional cast to satisfy strict mypy:

-        return self.device.get(SENSORS, {}).get(self.entity_description.key)  # Upstream consts
+        value = self.device.get(SENSORS, {}).get(self.entity_description.key)  # Upstream consts
+        return cast(int | float | None, value)

And add:

-from dataclasses import dataclass
+from dataclasses import dataclass
+from typing import cast
custom_components/plugwise/select.py (1)

135-143: Minor: normalize annotation spacing and tighten types for mypy.

  • Use spaced PEP 604 annotation for consistency.
  • Cast on dict access to avoid Any.
-    def current_option(self) -> str| None:
+    def current_option(self) -> str | None:
@@
-        return self.device.get(self.entity_description.key)
+        value = self.device.get(self.entity_description.key)
+        return cast(str | None, value)
@@
-        return self.device.get(self.entity_description.options_key, [])
+        value = self.device.get(self.entity_description.options_key, [])
+        return cast(list[str], value)

Add:

-from homeassistant.components.select import SelectEntity, SelectEntityDescription
+from homeassistant.components.select import SelectEntity, SelectEntityDescription
+from typing import cast
custom_components/plugwise/switch.py (1)

129-133: Constrain return type to Optional[bool] (avoid Any).

-        return self.device.get(SWITCHES, {}).get(self.entity_description.key) # Upstream const
+        value = self.device.get(SWITCHES, {}).get(self.entity_description.key)  # Upstream const
+        return cast(bool | None, value)

And at top:

-from typing import Any
+from typing import Any, cast
custom_components/plugwise/number.py (2)

124-136: Remove unused attribute and DRY nested gets.

  • self.actuator isn’t used elsewhere; drop to avoid confusion.
  • Optional: store ctrl = self.device.get(description.key, {}) to avoid repeating.
-        self.actuator = self.device.get(description.key)  # Upstream
         self.device_id = device_id
@@
-        self._attr_native_max_value = self.device.get(description.key, {}).get(UPPER_BOUND, 100.0)  # Upstream const
-        self._attr_native_min_value = self.device.get(description.key, {}).get(LOWER_BOUND, 0.0)  # Upstream const
+        ctrl = self.device.get(description.key, {})
+        self._attr_native_max_value = ctrl.get(UPPER_BOUND, 100.0)  # Upstream const
+        self._attr_native_min_value = ctrl.get(LOWER_BOUND, 0.0)  # Upstream const
@@
-        native_step = self.device.get(description.key, {}).get(RESOLUTION, 0.5)  # Upstream const
+        native_step = ctrl.get(RESOLUTION, 0.5)  # Upstream const

138-141: Align return type with Optional[float] explicitly.

-        return self.device.get(self.entity_description.key, {}).get("setpoint")
+        value = self.device.get(self.entity_description.key, {}).get("setpoint")
+        return cast(float | None, value)

Add if not present:

+from typing import cast
custom_components/plugwise/climate.py (1)

211-235: Guard SELECT_REGULATION_MODE to avoid KeyError.

REGULATION_MODES in _gateway_data doesn’t guarantee SELECT_REGULATION_MODE exists.

-            if REGULATION_MODES in self._gateway_data:
-                if self._gateway_data[SELECT_REGULATION_MODE] == HVACAction.COOLING:
+            if REGULATION_MODES in self._gateway_data:
+                selected = self._gateway_data.get(SELECT_REGULATION_MODE)
+                if selected == HVACAction.COOLING:
                     hvac_modes.append(HVACMode.COOL)
-                if self._gateway_data[SELECT_REGULATION_MODE] == HVACAction.HEATING:
+                if selected == HVACAction.HEATING:
                     hvac_modes.append(HVACMode.HEAT)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3bc196d and 6b16f49.

📒 Files selected for processing (7)
  • custom_components/plugwise/binary_sensor.py (2 hunks)
  • custom_components/plugwise/climate.py (6 hunks)
  • custom_components/plugwise/entity.py (1 hunks)
  • custom_components/plugwise/number.py (1 hunks)
  • custom_components/plugwise/select.py (1 hunks)
  • custom_components/plugwise/sensor.py (1 hunks)
  • custom_components/plugwise/switch.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
custom_components/plugwise/select.py (1)
custom_components/plugwise/entity.py (1)
  • device (91-93)
custom_components/plugwise/switch.py (2)
custom_components/plugwise/binary_sensor.py (1)
  • is_on (168-177)
custom_components/plugwise/entity.py (1)
  • device (91-93)
custom_components/plugwise/binary_sensor.py (2)
custom_components/plugwise/switch.py (1)
  • is_on (130-132)
custom_components/plugwise/entity.py (1)
  • device (91-93)
custom_components/plugwise/sensor.py (2)
custom_components/plugwise/number.py (1)
  • native_value (138-140)
custom_components/plugwise/entity.py (1)
  • device (91-93)
custom_components/plugwise/climate.py (1)
custom_components/plugwise/entity.py (1)
  • device (91-93)
custom_components/plugwise/number.py (2)
custom_components/plugwise/entity.py (1)
  • device (91-93)
custom_components/plugwise/sensor.py (1)
  • native_value (519-521)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Prepare
🔇 Additional comments (1)
custom_components/plugwise/climate.py (1)

129-134: LGTM: safer access + Optional returns.

Using .get(..., default) for bounds/steps and returning float | None for temperatures/preset access matches HA patterns and avoids KeyError.

Please confirm HA Core tests for these entities run clean with these Optional returns (no assumptions about non-None downstream).

Also applies to: 169-172, 174-181, 183-189, 191-197, 296-299

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error while testing for Development HA-core:

⚠️ Warning: Incompatible while testing against dev HA-core.
✔️ Success: No problem with testing against released HA-core.

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error while testing for Development HA-core:

⚠️ Warning: Incompatible while testing against dev HA-core.
✔️ Success: No problem with testing against released HA-core.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
custom_components/plugwise/number.py (2)

128-131: Cast bounds to float to satisfy mypy and avoid type drift

ctrl.get(...) is Any; assigning to _attr_native_max_value/_attr_native_min_value (float | None) can still trip mypy. Cast to float to make the type explicit.

Apply:

-        ctrl = self.device.get(description.key, {})
-        self._attr_native_max_value = ctrl.get(UPPER_BOUND, 100.0)  # Upstream const
-        self._attr_native_min_value = ctrl.get(LOWER_BOUND, 0.0)  # Upstream const
+        ctrl = self.device.get(description.key, {})
+        self._attr_native_max_value = float(ctrl.get(UPPER_BOUND, 100.0))  # Upstream const
+        self._attr_native_min_value = float(ctrl.get(LOWER_BOUND, 0.0))  # Upstream const

132-136: Ensure step is a float before clamping

Same mypy concern for native_step. Casting also guards against accidental non‑float values.

-        native_step = ctrl.get(RESOLUTION, 0.5)  # Upstream const
+        native_step = float(ctrl.get(RESOLUTION, 0.5))  # Upstream const
         if description.key != TEMPERATURE_OFFSET:  # Upstream const
             native_step = max(native_step, 0.5)
         self._attr_native_step = native_step
custom_components/plugwise/climate.py (2)

129-134: Cast thermostat limits/step to float to quiet mypy

get(...) returns Any; explicit float(...) avoids Optional[Any] flowing into attr* fields.

-        self._attr_max_temp = min(self.device.get(THERMOSTAT, {}).get(UPPER_BOUND, 35.0), 35.0)
-        self._attr_min_temp = self.device.get(THERMOSTAT, {}).get(LOWER_BOUND, 0.0)
+        self._attr_max_temp = min(float(self.device.get(THERMOSTAT, {}).get(UPPER_BOUND, 35.0)), 35.0)
+        self._attr_min_temp = float(self.device.get(THERMOSTAT, {}).get(LOWER_BOUND, 0.0))
         # Ensure we don't drop below 0.1
-        self._attr_target_temperature_step = max(
-            self.device.get(THERMOSTAT, {}).get(RESOLUTION, 0.5), 0.1
-        )
+        self._attr_target_temperature_step = max(
+            float(self.device.get(THERMOSTAT, {}).get(RESOLUTION, 0.5)), 0.1
+        )

150-153: Always set preset_modes attribute for consistency

If preset_modes is an empty list, the feature remains disabled (good), but _attr_preset_modes stays unset. Setting it explicitly to None or [] improves clarity.

-        if presets := self.device.get("preset_modes", None):  # can be NONE
+        presets = self.device.get("preset_modes", None)  # can be None or []
+        if presets:
             self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
-        self._attr_preset_modes = presets
+        self._attr_preset_modes = presets
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6b16f49 and 6681a04.

📒 Files selected for processing (4)
  • custom_components/plugwise/climate.py (6 hunks)
  • custom_components/plugwise/entity.py (1 hunks)
  • custom_components/plugwise/number.py (1 hunks)
  • custom_components/plugwise/select.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • custom_components/plugwise/entity.py
  • custom_components/plugwise/select.py
🧰 Additional context used
🧬 Code graph analysis (2)
custom_components/plugwise/number.py (2)
custom_components/plugwise/entity.py (1)
  • device (91-93)
custom_components/plugwise/sensor.py (1)
  • native_value (519-521)
custom_components/plugwise/climate.py (2)
custom_components/plugwise/entity.py (1)
  • device (91-93)
tests/components/plugwise/conftest.py (1)
  • cooling_present (49-54)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Setup for HA-core (release/master)
  • GitHub Check: Prepare and validate pre-commit
🔇 Additional comments (5)
custom_components/plugwise/number.py (1)

138-141: LGTM: Optional-safe native_value

Returning Optional aligns with NumberEntity expectations and avoids KeyError.

custom_components/plugwise/climate.py (4)

169-172: LGTM: Optional-safe sensor access

Avoids KeyError; Optional return matches ClimateEntity expectations.


174-197: LGTM: Optional-safe target temperatures

All target temperature properties now return Optional[float]; aligns with HA typing and prevents KeyError.


225-235: LGTM: Safer hvac_modes derivation

Using .get(...) for AVAILABLE_SCHEDULES and SELECT_REGULATION_MODE avoids KeyError and keeps types narrow.


301-302: LGTM: Safe preset check in HomeKit emulation

Guarded ACTIVE_PRESET access avoids KeyError.

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error while testing for Development HA-core:

⚠️ Warning: Incompatible while testing against dev HA-core.
✔️ Success: No problem with testing against released HA-core.

@sonarqubecloud
Copy link

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error while testing for Development HA-core:

⚠️ Warning: Incompatible while testing against dev HA-core.
✔️ Success: No problem with testing against released HA-core.

@bouwew bouwew marked this pull request as ready for review September 21, 2025 08:36
@bouwew bouwew requested a review from a team as a code owner September 21, 2025 08:36
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
custom_components/plugwise/climate.py (1)

288-295: Pass Plugwise regulation-mode strings to set_regulation_mode (not HA enums/actions)

set_regulation_mode expects Plugwise regulation-mode strings (e.g. "heating", "cooling", "off"); do not pass HVACAction objects or raw HA enum objects.

  • Location: custom_components/plugwise/climate.py lines 288-295 (also check 296-304).
  • Fix: convert/map before calling (e.g. set_regulation_mode(hvac_mode.value) or map HVACMode/HVACAction → Plugwise string); ensure self._previous_mode is a regulation-mode string, not an HVACAction.
🧹 Nitpick comments (3)
custom_components/plugwise/climate.py (3)

150-153: Preset handling is safe; optionally set explicit None

Setting features only when presets exist is fine. Optionally set _attr_preset_modes = None in the else-path for explicitness (not required).


201-214: HVAC mode coercion hardened correctly

Coercion via HVACMode(mode) with fallbacks is good. Minor: caching self.hvac_modes in a local var avoids recomputation (optional).


225-239: Edge case: hvac_modes may omit both HEAT/COOL when selected is unknown

If cooling_present is True, REGULATION_MODES exists, but SELECT_REGULATION_MODE is missing/unknown, neither HEAT nor COOL is added. Consider exposing both to avoid an empty set and to align with hvac_mode’s HEAT fallback.

Apply:

-            if REGULATION_MODES in self._gateway_data:
-                selected = self._gateway_data.get(SELECT_REGULATION_MODE)
-                if selected == HVACAction.COOLING:
-                    hvac_modes.append(HVACMode.COOL)
-                if selected == HVACAction.HEATING:
-                    hvac_modes.append(HVACMode.HEAT)
+            if REGULATION_MODES in self._gateway_data:
+                selected = self._gateway_data.get(SELECT_REGULATION_MODE)
+                if selected == HVACAction.COOLING:
+                    hvac_modes.append(HVACMode.COOL)
+                elif selected == HVACAction.HEATING:
+                    hvac_modes.append(HVACMode.HEAT)
+                else:
+                    # Unknown selection: expose both to keep UI functional
+                    hvac_modes.extend([HVACMode.HEAT, HVACMode.COOL])

Also applies to: 230-234

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d8b7e61 and 2b8bd9d.

📒 Files selected for processing (1)
  • custom_components/plugwise/climate.py (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
custom_components/plugwise/climate.py (1)
custom_components/plugwise/entity.py (1)
  • device (91-93)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Setup for HA-core (release/master)
🔇 Additional comments (7)
custom_components/plugwise/climate.py (7)

109-111: Type fix for _homekit_mode is correct; align _previous_mode typing too

Good change to HVACMode | None. To tighten types further, annotate _previous_mode as HVACAction (it holds HVACAction values).

Apply:

-    _previous_mode: str = HVACAction.HEATING  # Upstream
+    _previous_mode: HVACAction = HVACAction.HEATING  # Upstream

129-134: Bounds/step guarded lookups LGTM

Using .get with sane defaults addresses mypy and runtime KeyError risks.


169-172: Optional current_temperature return type is appropriate

Guarded access + Optional return matches HA expectations.


174-181: Optional target_temperature LGTM


183-189: Optional target_temperature_high LGTM


191-196: Optional target_temperature_low LGTM


256-257: preset_mode getter uses safe access; good

Copy link
Member

@CoMPaTech CoMPaTech left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added todo for #926

@bouwew bouwew merged commit f0c1541 into main Sep 21, 2025
13 checks passed
@bouwew bouwew deleted the fix-mypy-errors branch September 21, 2025 08:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants